/**
 * @fileoverview This file contains the odfStreamConverter object.
 *
 * This object has to act as an observer so it can be started and stopped (nsIRequestObserver).<br>
 * This object also needs to act as a streamListener so it can be called when data becomes available (nsIStreamListener;
 * inherits from nsIRequestObserver). <br>
 * This object also needs to act as a stream converter, so it can perform conversion on the data. <br>
 *
 * <pre>
 * nsISupports interface: IID = 00000000-0000-0000-c000-000000000046
 * - void QueryInterface ( nsIIDRef uuid , out nsQIResult* result )
 * nsIRequestObserver interface: IID = fd91e2e0-1481-11d3-9333-00104ba0fd40
 * - void onStartRequest ( nsIRequest aRequest , nsISupports aContext )
 * - void onStopRequest ( nsIRequest aRequest , nsISupports aContext , nsresult aStatusCode )
 * nsIStreamListener interface: IID = 1a637020-1482-11d3-9333-00104ba0fd40
 * - void onDataAvailable ( nsIRequest aRequest , nsISupports aContext , nsIInputStream aInputStream , PRUint32 aOffset , PRUint32 aCount )
 * nsIStreamConverter interface: IID = 0b6e2c69-5cf5-48b0-9dfd-c95950e2cc7b
 * - void asyncConvertData ( string aFromType , string aToType , nsIStreamListener aListener , nsISupports aCtxt )
 * - nsIInputStream convert ( nsIInputStream aFromStream , string aFromType , string aToType , nsISupports aCtxt )
 * </pre>
 *
 * Some of the codes used were based on the following existing addons for Firefox.
 * 1. ODFReader 0.2.1 (Updated October 24, 2006)
 *     https://addons.mozilla.org/en-US/firefox/addon/1888
 *    @author Talin(C) 2005-6
 *    @author Alex Hudson (C) 2006
 *    Released under the MPL/LGPL/GPL tri-license
 * 2. OdfPlugin 0.17b (Updated October 23, 2007)
 *     https://addons.mozilla.org/en-US/firefox/addon/5907
 *    @author David Staessens (02-05-07)
 * - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - = - =
 *
 * @author last modified by: Jamie L. Vargas (2009/08/03)
 * Released under GPLv3. See file gpl-3.0.txt included in this package.
 */

/**
 * @constructor
 */
function odfStreamConverter() {
    this.wrappedJSObject = this;
}

/**
 * (nsIStreamListener to nsIStreamListener) This method is called to start an Asynchronous conversion of the data. The
 * asynchronous conversion is non blocking and needs a listener which is supplied here.
 *
 * Converts data arriving via the converter's nsIStreamListener::OnDataAvailable() method from one type to another,
 * pushing the converted data out to the caller via aListener::OnDataAvailable().
 *
 * Use this method when you want to proxy (and convert) nsIStreamListener callbacks asynchronously.
 *
 * @param {string} aFromType The MIME type of the original/raw data.
 * @param {string} aToType The MIME type of the converted data.
 * @param {nsIStreamListener} aListener The listener who receives the converted data.
 * @param {nsISupports} aCtxt Either an opaque context, or a converter specific context (implementation specific).
 * @return {void}
 */
odfStreamConverter.prototype.asyncConvertData = function(aFromType, aToType, aListener, aCtxt) {
    logDebug("odfStreamConverter::asyncConvertData", "Storing listener so we can notify it when needed.");

    // we store a pointer to the listener that called this method, so we can notify the listener when data is available
    this.listener = aListener;
}

/**
 * (Stream to Stream) This method is called to start a Synchronous conversion of the data (a stream of one type, to a
 * stream of another type). This method is blocking.
 *
 * @param {nsIInputStream} aFromStream The stream representing the original/raw data.
 * @param {string} aFromType The MIME type of aFromStream.
 * @param {string} aToType The MIME type of the returned stream.
 * @param {nsISupports} aCtxt Either an opaque context, or a converter specific context (implementation specific).
 * @return {nsIInputStream} The converted stream. NOTE: The returned stream may not already be converted. An efficient
 *         stream converter implementation will converter data on demand rather than buffering the converted data until
 *         it is used.
 *
 * @throws NS_ERROR_NOT_IMPLEMENTED This method is not implemented.
 */
odfStreamConverter.prototype.convert = function(aFromStream, aFromType, aToType, aCtxt) {
    logDebug("odfStreamConverter::convert", "Synchronous converting data not implemented!");

    // null implementation
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}

/**
 * This method is called once at the start of the conversion It is responsible for preliminary things like loading the
 * stylesheet, creating a tmp file to stream the odf file into etc.
 *
 * @param {nsIRequest} aRequest
 * @param {nsISupports} aContext
 * @return {void}
 */
odfStreamConverter.prototype.onStartRequest = function(aRequest, aContext) {
    logDebug("odfStreamConverter::onStartRequest", "----START----");

    logDebug("odfStreamConverter::onStartRequest", " nsIRequest.name: " + aRequest.name);

    // this.uri = aRequest.name
    //    should also return the request's uri but can't be used to convert the request into a channel
    this.uri = aRequest.QueryInterface(Ci.nsIChannel).URI.spec;

    /*
     * Change the request in a channel by adding extra parameters A channel allows more control on managing request by
     * adding a contentType, so the listener knows how the stream should be read, in this case as html.
     */
    this.channel = aRequest;

    // removing next line will make mozilla crash, because the listener doesn't know what to do with the stream
    this.channel.contentType = "text/html";

    logDebug("odfStreamConverter::onStartRequest", "Notifying listener.");
    // Notify the listener that started the conversion that onStartRequest is Completed
    this.listener.onStartRequest(this.channel, aContext);

    //Construct the document name using the URI
    var documentName = this.uri;
    documentName = documentName.substring(documentName.lastIndexOf('/') + 1, documentName.length);
    // make the documentName URL safe
    documentName = documentName.replace('%20', '');  // SPACE    %20
    documentName = documentName.replace('%22', '');  // "        %22
    documentName = documentName.replace('%23', '');  // #        %23
    documentName = documentName.replace('%24', '');  // $        %24
    documentName = documentName.replace('%25', '');  // %        %25
    documentName = documentName.replace('%26', '');  // &        %26
    documentName = documentName.replace('%27', '');  // '        %27
    documentName = documentName.replace('%3b', '');  // ;        %3B
    documentName = documentName.replace('%3B', '');  // ;        %3B
    documentName = documentName.replace('%3c', '');  // <        %3C
    documentName = documentName.replace('%3C', '');  // <        %3C
    documentName = documentName.replace('%3d', '');  // =        %3D
    documentName = documentName.replace('%3D', '');  // =        %3D
    documentName = documentName.replace('%3e', '');  // >        %3E
    documentName = documentName.replace('%3E', '');  // >        %3E
    documentName = documentName.replace('%3f', '');  // ?        %3F
    documentName = documentName.replace('%3F', '');  // ?        %3F
    documentName = documentName.replace('%40', '');  // @        %40
    documentName = documentName.replace('%5b', '');  // [        %5B
    documentName = documentName.replace('%5B', '');  // [        %5B
    documentName = documentName.replace('%5c', '');  // \        %5C
    documentName = documentName.replace('%5C', '');  // \        %5C
    documentName = documentName.replace('%5d', '');  // ]        %5D
    documentName = documentName.replace('%5D', '');  // ]        %5D
    documentName = documentName.replace('%5e', '');  // ^        %5E
    documentName = documentName.replace('%5E', '');  // ^        %5E
    documentName = documentName.replace('%60', '');  // `        %60
    documentName = documentName.replace('%7b', '');  // {        %7B
    documentName = documentName.replace('%7B', '');  // {        %7B
    documentName = documentName.replace('%7c', '');  // |        %7C
    documentName = documentName.replace('%7C', '');  // |        %7C
    documentName = documentName.replace('%7d', '');  // }        %7D
    documentName = documentName.replace('%7D', '');  // }        %7D
    documentName = documentName.replace('%7e', '');  // ~        %7E
    documentName = documentName.replace('%7E', '');  // ~        %7E

    logDebug("odfStreamConverter::onStartRequest", "Creating tmp file to store zipped data into.");
    // Create a temp file in the temp dir to store the zip
    this.zipFile = gTempDir.clone();
    this.zipFile.append(documentName);
    this.zipFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0664);

    logDebug("odfStreamConverter::onStartRequest", "Create an output stream on this zipFile.");
    // Create an output stream on the tmp zipFile, write, create, truncate
    this.ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
    this.ostream.init(this.zipFile, 0x02 | 0x08 | 0x20, 0664, 0);

    logDebug("odfStreamConverter::onStartRequest", "----END----");
}

/**
 * This method is called once at the end of the conversion.
 *
 * It is responsible for the things that need to be done when the stream has ended, like removing the tmp zip file,
 * performing the conversion, sending the resulting data stream to the listener
 *
 * @param {nsIRequest} aRequest
 * @param {nsISupports} aContext
 * @param {nsresult} aStatusCode
 * @return {void}
 */
odfStreamConverter.prototype.onStopRequest = function(aRequest, aContext, aStatusCode) {
    logDebug("odfStreamConverter::onStopRequest", "----START----");

    logDebug("odfStreamConverter::onStopRequest", "Closing the output stream on the tmp zipfile.");
    this.ostream.close();

    logDebug("odfStreamConverter::onStopRequest", "Starting uzipping and converting.");
    if (!oFCLoaded) {
        throw Components.results.NS_ERROR_NOT_INITIALIZED;
    }

    try {
        // Filename for display on the titlebar, in case the title meta-tag is not available
        var fileName = aRequest.QueryInterface(Ci.nsIChannel).URI.spec;
        fileName = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.length);
        fileName = fileName.replace('%20', ' ');   // SPACE    %20
        fileName = fileName.replace('%22', '"');   // "        %22
        fileName = fileName.replace('%23', '#');   // #        %23
        fileName = fileName.replace('%60', '`');   // `        %60
        fileName = fileName.replace('%3b', ';');   // ;        %3B
        fileName = fileName.replace('%3B', ';');   // ;        %3B
        fileName = fileName.replace('%3c', '<');   // <        %3C
        fileName = fileName.replace('%3C', '<');   // <        %3C
        fileName = fileName.replace('%3e', '>');   // >        %3E
        fileName = fileName.replace('%3E', '>');   // >        %3E
        fileName = fileName.replace('%3f', '?');   // ?        %3F
        fileName = fileName.replace('%3F', '?');   // ?        %3F
        fileName = fileName.replace('%5b', '[');   // [        %5B
        fileName = fileName.replace('%5B', '[');   // [        %5B
        fileName = fileName.replace('%5d', ']');   // ]        %5D
        fileName = fileName.replace('%5D', ']');   // ]        %5D
        fileName = fileName.replace('%5e', '^');   // ^        %5E
        fileName = fileName.replace('%5E', '^');   // ^        %5E
        fileName = fileName.replace('%7b', '{');   // {        %7B
        fileName = fileName.replace('%7B', '{');   // {        %7B
        fileName = fileName.replace('%7d', '}');   // }        %7D
        fileName = fileName.replace('%7D', '}');   // }        %7D

        // Transform the ODF
        odfContext = odfFileConverter.transform(this.zipFile, fileName);
    } catch (error) {
        logDebug("odfStreamConverter::onStopRequest::transform", error);
        throw error;
    }

    // SetData - assign data to the input stream (copied on assignment).
    logDebug("odfStreamConverter::onStopRequest", "Writing converted data to the inputStream.");
    var stringInputStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
    stringInputStream.setData(odfContext, odfContext.length);

    logDebug("odfStreamConverter::onStopRequest", "Passing the inputStream to the main content listener.");
    this.channel.contentCharset = "UTF-8";
    this.listener.onDataAvailable(this.channel, aContext, stringInputStream, 0, odfContext.length);
    this.listener.onStopRequest(this.channel, aContext, aRequest.status);

    // delete the tmp Zip File
    //this.zipFile.remove(false);

    logDebug("odfStreamConverter::onStopRequest", "----END----");
}

/**
 * This method is called when data becomes available, in the object that invoked this StreamConverter. The data is
 * passed in a nsIInputStream, and we write this data to our tmp zip file.
 *
 * This method is called when the next chunk of data for the ongoing request may be read without blocking the calling
 * thread. The data can be read from the nsIInputStream object passed as the argument to this method. Note: Your
 * implementation of this method must read exactly aCount bytes of data before returning. Furthermore, throwing an
 * exception will cancel the request.
 *
 * @param {nsIRequest} aRequest An nsIRequest indicating the source of the data.
 * @param {nsISupports} aContext A user-defined context value.
 * @param {nsIInputStream} aInputStream An nsIInputStream from which the data is to be read.
 * @param {unsigned long} (PRUint32) aOffset The offset into the stream at which to begin reading. This is the number of
 *            bytes that were sent in all previous onDataAvailable() calls combined. If the sum is greater than or equal
 *            to the number of bytes that can be represented in an unsigned long (2^32 bytes), the value will be
 *            PR_UINT32_MAX (2^32 - 1).
 * @param {unsigned long} (PRUint32) aCount The number of bytes available for reading. You must read all of these bytes
 *            before returning to the caller.
 * @return {void}
 */
odfStreamConverter.prototype.onDataAvailable = function(aRequest, aContext, aInputStream, aOffset, aCount) {
    logDebug("odfStreamConverter::onDataAvailable", "----START----");

    logDebug("odfStreamConverter::onDataAvailable", "Writing data to the zipfile.");

    // When data from the .odf file becomes available and is downloaded, it is put into an inputStream.
    // This inputStream is passed to this method, so it can be written to the tmp Zipped file.
    binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
    binaryInputStream.setInputStream(aInputStream);

    var inputData = binaryInputStream.readBytes(aCount);

    this.ostream.write(inputData, inputData.length);

    logDebug("odfStreamConverter::onDataAvailable", "----END----");
}

var oSCLoaded = true;
